/**
 * 
 */
package edu.ubc.javascript;

import static edu.ubc.javascript.ContextCheck.isVariableReferenceName;

import java.util.IdentityHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Stack;

import com.google.javascript.jscomp.mozilla.rhino.ast.Assignment;
import com.google.javascript.jscomp.mozilla.rhino.ast.AstNode;
import com.google.javascript.jscomp.mozilla.rhino.ast.FunctionNode;
import com.google.javascript.jscomp.mozilla.rhino.ast.Name;
import com.google.javascript.jscomp.mozilla.rhino.ast.UnaryExpression;

public class FunctionNodeClosureInfoVisitor extends TransformingNodeVisitor {

	private boolean OPTIMIZED = true;
	private List<FunctionNodeScopeInfo> topLevelFunctions = new LinkedList<FunctionNodeScopeInfo>();
	public List<FunctionNodeScopeInfo> getTopLevelFunctions() { return topLevelFunctions; }
	
	private Map<FunctionNode, FunctionNodeScopeInfo> info = new IdentityHashMap<FunctionNode, FunctionNodeScopeInfo>();
	public Map<FunctionNode, FunctionNodeScopeInfo> getInfo() { return info; }
	public boolean isOnClosurePath(FunctionNode fn) {
		if(OPTIMIZED) {
			return getInfo().get(fn).isOnClosurePath();
		} else {
			return true;
		}
	}
	
	private Stack<FunctionNode> stack = new Stack<FunctionNode>();

	public FunctionNodeClosureInfoVisitor(ReflectiveAstTransformer tx) {
		super(tx);
	}
	
	@Override
	public boolean visit(AstNode astNode) {
		if(astNode instanceof FunctionNode) {
			FunctionNode fn = (FunctionNode) astNode;
			FunctionNodeScopeInfo fnci = new FunctionNodeScopeInfo(fn);
			if(stack.isEmpty()) {
				topLevelFunctions.add(fnci);
			}
			for(FunctionNode item : stack) {
				FunctionNodeScopeInfo ancestor = info.get(item);
				ancestor.setHasChildren();
			}
			stack.push(fn);
			info.put(fn, fnci);
			boolean returnVal = visitFunctionNode(fn);
			stack.pop();
			return returnVal;
		} else if(astNode instanceof Name) {
			return visitName((Name) astNode, false);
		}
		return true;
	}
	
	private boolean visitName(Name name, boolean isParam) {
		if(!isParam && !isVariableReferenceName(name)) {
			return true;
		}
		if(!stack.isEmpty()) {
			FunctionNode current = stack.peek();
			FunctionNodeScopeInfo fnci = info.get(current);
			Tag t = TagManager.getUniqueTag(name, "isClosed");
			if(t != null) {
				fnci.addClosureVariable(name.getIdentifier());
				System.out.print("");
			}
		}
		Tag tag = TagManager.getUniqueTag(name, "scope");
		if(tag != null) {
		
			String scopeString = tag.getValue().toString();
			if(!stack.isEmpty() && scopeString.startsWith("up")) {
				FunctionNode current = stack.peek();
				FunctionNodeScopeInfo fnci = info.get(current);
				
				Integer height = Integer.parseInt(scopeString.substring(2));
				FunctionNode parent = stack.get((stack.size()-1) - height);
				
				Tag tr = TagManager.getUniqueTag(name, "multipleRefs");
				if(!lvalue(name) && (tr == null)) {
					fnci.addClosedVar(name.getString(), height);
				} else {
					fnci.prohibitClosureVar(name.getString());
				}
				FunctionNodeScopeInfo parentInfo = info.get(parent);
				fnci.addParent(parentInfo);
				parentInfo.addChild(fnci);
				for(int i=1; i <= height; i++) {
					FunctionNode onPath = stack.get((stack.size()-1) - i);
					FunctionNodeScopeInfo onPathInfo = info.get(onPath);
					onPathInfo.setOnClosurePath();
				}
			} 
		}
		return true;
	}
	
	private boolean lvalue(Name n) {
		AstNode parent = n.getParent();
		if(parent instanceof UnaryExpression) {
			return true;
		}
		if(parent instanceof Assignment) {
			if(((Assignment) parent).getLeft() == n) {
				return true;
			}
		}
		return false;
	}
	
	private boolean visitFunctionNode(FunctionNode fn) {
		for(AstNode node : fn.getParams()) {
			visitName((Name) node, true);
		}
		fn.getBody().visit(this);
		return false;
	}
	
}